#include <QTcpSocket>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslSocket>
#include <QTcpServer>

#include <QCryptographicHash>
#include <QTcpSocket>
#include <QStringBuilder>
#include <QStringList>
#include <QHostAddress>


#include "QtHttpClientWrapper.h"
#include "QtHttpRequest.h"
#include "QtHttpReply.h"
#include "QtHttpServer.h"
#include "QtHttpHeader.h"
#include "WebSocketClient.h"
#include "WebJsonRpc.h"

#define REQ "request="
#define RPC "json-rpc"

const QByteArray& QtHttpClientWrapper::CRLF = QByteArrayLiteral("\r\n");

QtHttpClientWrapper::QtHttpClientWrapper(
	QTcpSocket* sock, const bool& localConnection, QtHttpServer* parent)
	: QObject(parent)
	, m_guid("")
	, m_parsingStatus(AwaitingRequest)
	, m_sockClient(sock)
	, m_currentRequest(Q_NULLPTR)
	, m_serverHandle(parent)
	, m_localConnection(localConnection)
	, m_websocketClient(nullptr)
	, m_webJsonRpc(nullptr)
{
	connect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived);
}

QString QtHttpClientWrapper::getGuid(void)
{
	if (m_guid.isEmpty())
	{
		m_guid = QString::fromLocal8Bit(
			QCryptographicHash::hash(
				QByteArray::number((quint64)(this)),
				QCryptographicHash::Md5
			).toHex()
		);
	}

	return m_guid;
}


void QtHttpClientWrapper::onReplySendDataRequested(void)
{
	QtHttpReply* reply = qobject_cast<QtHttpReply*> (sender());
	if (reply != Q_NULLPTR && m_sockClient != nullptr && m_sockClient->state() == QAbstractSocket::ConnectedState)
	{
		// content raw data
		QByteArray data = reply->getRawData();

		if (reply->useChunked())
		{
			data.prepend(QByteArray::number(data.size(), 16) % CRLF);
			data.append(CRLF);
			reply->resetRawData();
		}
		if (m_sockClient->state() == QAbstractSocket::ConnectedState)
		{
			m_sockClient->write(data);
			m_sockClient->flush();
		}
	}
}

void QtHttpClientWrapper::onClientDataReceived(void)
{
	if (m_sockClient != Q_NULLPTR)
	{
		while (m_sockClient->bytesAvailable())
		{
			QByteArray line = m_sockClient->readLine();

			switch (m_parsingStatus) // handle parsing steps
			{
			case AwaitingRequest: // "command url version" × 1
			{
				QString str = QString::fromUtf8(line).trimmed();
				QStringList parts = str.split(SPACE, Qt::SkipEmptyParts);
				if (parts.size() == 3)
				{
					QString command = parts.at(0);
					QString url = parts.at(1);
					QString version = parts.at(2);

					if (version == QtHttpServer::HTTP_VERSION)
					{
						m_currentRequest = new QtHttpRequest(this, m_serverHandle);
						m_currentRequest->setClientInfo(m_sockClient->localAddress(), m_sockClient->peerAddress());
						m_currentRequest->setUrl(QUrl(url));
						m_currentRequest->setCommand(command);
						m_parsingStatus = AwaitingHeaders;
					}
					else
					{
						m_parsingStatus = ParsingError;
						//qWarning () << "Error : unhandled HTTP version :" << version;
					}
				}
				else
				{
					m_parsingStatus = ParsingError;
					//qWarning () << "Error : incorrect HTTP command line :" << line;
				}

				break;
			}
			case AwaitingHeaders: // "header: value" × N (until empty line)
			{
				QByteArray raw = line.trimmed();

				if (!raw.isEmpty()) // parse headers
				{
					int pos = raw.indexOf(COLON);

					if (pos > 0)
					{
						QByteArray header = raw.left(pos).trimmed();
						QByteArray value = raw.mid(pos + 1).trimmed();
						m_currentRequest->addHeader(header, value);
						if (header == QtHttpHeader::ContentLength)
						{
							bool ok = false;
							const int len = value.toInt(&ok, 10);
							if (ok)
							{
								m_currentRequest->addHeader(QtHttpHeader::ContentLength, QByteArray::number(len));
							}
						}
					}
					else
					{
						m_parsingStatus = ParsingError;
						qWarning() << "Error : incorrect HTTP headers line :" << line;
					}
				}
				else // end of headers
				{
					if (m_currentRequest->getHeader(QtHttpHeader::ContentLength, 0) > 0)
					{
						m_parsingStatus = AwaitingContent;
					}
					else
					{
						m_parsingStatus = RequestParsed;
					}
				}

				break;
			}
			case AwaitingContent: // raw data × N (until EOF ??)
			{
				m_currentRequest->appendRawData(line);

				if (m_currentRequest->getRawDataSize() == m_currentRequest->getHeader(QtHttpHeader::ContentLength, 0))
				{
					m_parsingStatus = RequestParsed;
				}

				break;
			}
			default:
			{
				break;
			}
			}

			switch (m_parsingStatus) // handle parsing status end/error
			{
			case RequestParsed: // a valid request has ben fully parsed
			{
				// Catch websocket header "Upgrade"
				if (m_currentRequest->getHeader(QtHttpHeader::Upgrade).toLower() == "websocket")
				{
					if (m_websocketClient == Q_NULLPTR)
					{
						// disconnect this slot from socket for further requests
						disconnect(m_sockClient, &QTcpSocket::readyRead, this, &QtHttpClientWrapper::onClientDataReceived);
						// disabling packet bunching
						m_sockClient->setSocketOption(QAbstractSocket::LowDelayOption, 1);
						m_sockClient->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
						m_websocketClient = new WebSocketClient(m_currentRequest, m_sockClient, m_localConnection, this);
					}

					break;
				}

				// add  post data to request and catch /jsonrpc subroute url
				QString path = m_currentRequest->getUrl().path();
				QString query = (m_currentRequest->getUrl().hasQuery()) ? m_currentRequest->getUrl().query(QUrl::FullyDecoded) : "";
				QStringList uri_parts = path.split('/', Qt::SkipEmptyParts);
				bool getCallback = (m_currentRequest->getCommand() == "GET") && !uri_parts.empty() &&
					uri_parts.at(0) == RPC &&
					(query.indexOf(REQ, Qt::CaseInsensitive) == 0);

				if (m_currentRequest->getCommand() == "POST" || getCallback)
				{
					QtHttpPostData  postData;

					if (getCallback)
					{
						query = query.remove(0, QString(REQ).length());
						if (query.trimmed().length() == 0)
						{
							query = "{\"command\":\"help\"}";
						}
					}
					else
					{
						QByteArray data = m_currentRequest->getRawData();
						QList<QByteArray> parts = data.split('&');

						query = "";
						for (int i = 0; i < parts.size(); ++i)
						{
							QList<QByteArray> keyValue = parts.at(i).split('=');
							QByteArray value;

							if (keyValue.size() > 1)
							{
								value = QByteArray::fromPercentEncoding(keyValue.at(1));
							}
							postData.insert(QString::fromUtf8(keyValue.at(0)), value);
						}
					}

					m_currentRequest->setPostData(postData);

					// catch /jsonrpc in url, we need async callback, StaticFileServing is sync						
					if (getCallback || (!uri_parts.empty() && uri_parts.at(0) == RPC))
					{
						if (m_webJsonRpc == Q_NULLPTR)
						{
							m_webJsonRpc = new WebJsonRpc(m_currentRequest, m_serverHandle, m_localConnection, this);
						}

						m_webJsonRpc->handleMessage(m_currentRequest, query);
						break;
					}
				}

				QtHttpReply reply(m_serverHandle);
				connect(&reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested);
				connect(&reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested);
				emit m_serverHandle->requestNeedsReply(m_currentRequest, &reply); // allow app to handle request
				m_parsingStatus = sendReplyToClient(&reply);

				break;
			}
			case ParsingError: // there was an error durin one of parsing steps
			{
				m_sockClient->readAll(); // clear remaining buffer to ignore content
				QtHttpReply reply(m_serverHandle);
				reply.setStatusCode(QtHttpReply::BadRequest);
				reply.appendRawData(QByteArrayLiteral("<h1>Bad Request (HTTP parsing error) !</h1>"));
				reply.appendRawData(CRLF);
				m_parsingStatus = sendReplyToClient(&reply);

				break;
			}
			default:
			{
				break;
			}
			}
		}
	}
}


void QtHttpClientWrapper::closeConnection()
{
	// probably filter for request to follow http spec
	if (m_currentRequest != Q_NULLPTR)
	{
		QtHttpReply reply(m_serverHandle);
		reply.setStatusCode(QtHttpReply::StatusCode::Forbidden);

		connect(&reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested, Qt::UniqueConnection);
		connect(&reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested, Qt::UniqueConnection);

		m_parsingStatus = sendReplyToClient(&reply);
	}
	m_sockClient->close();
}

void QtHttpClientWrapper::onReplySendHeadersRequested(void)
{
	QtHttpReply* reply = qobject_cast<QtHttpReply*> (sender());

	if (reply != Q_NULLPTR)
	{
		QByteArray data;
		// HTTP Version + Status Code + Status Msg
		data.append(QtHttpServer::HTTP_VERSION.toUtf8());
		data.append(SPACE);
		data.append(QByteArray::number(reply->getStatusCode()));
		data.append(SPACE);
		data.append(QtHttpReply::getStatusTextForCode(reply->getStatusCode()));
		data.append(CRLF);

		if (reply->useChunked()) // Header name: header value
		{
			static const QByteArray& CHUNKED = QByteArrayLiteral("chunked");
			reply->addHeader(QtHttpHeader::TransferEncoding, CHUNKED);
		}
		else
		{
			reply->addHeader(QtHttpHeader::ContentLength, QByteArray::number(reply->getRawDataSize()));
		}

		const QList<QByteArray>& headersList = reply->getHeadersList();

		foreach(const QByteArray & header, headersList)
		{
			data.append(header);
			data.append(COLON);
			data.append(SPACE);
			data.append(reply->getHeader(header));
			data.append(CRLF);
		}

		// empty line
		data.append(CRLF);
		m_sockClient->write(data);
		m_sockClient->flush();
	}
}

void QtHttpClientWrapper::sendToClientWithReply(QtHttpReply* reply)
{
	connect(reply, &QtHttpReply::requestSendHeaders, this, &QtHttpClientWrapper::onReplySendHeadersRequested, Qt::UniqueConnection);
	connect(reply, &QtHttpReply::requestSendData, this, &QtHttpClientWrapper::onReplySendDataRequested, Qt::UniqueConnection);
	m_parsingStatus = sendReplyToClient(reply);
}

QtHttpClientWrapper::ParsingStatus QtHttpClientWrapper::sendReplyToClient(QtHttpReply* reply)
{
	if (reply != Q_NULLPTR)
	{
		if (!reply->useChunked())
		{
			//reply->appendRawData (CRLF);
			// send all headers and all data in one shot
			reply->requestSendHeaders();
			reply->requestSendData();
		}
		else
		{
			// last chunk
			m_sockClient->write("0" % CRLF % CRLF);
			m_sockClient->flush();
		}

		if (m_currentRequest != Q_NULLPTR)
		{
			static const QByteArray& CLOSE = QByteArrayLiteral("close");

			if (m_currentRequest->getHeader(QtHttpHeader::Connection).toLower() == CLOSE)
			{
				// must close connection after this request
				m_sockClient->close();
			}

			m_currentRequest->deleteLater();
			m_currentRequest = Q_NULLPTR;
		}
	}

	return AwaitingRequest;
}

